Шаг 1. Установка Nginx
sudo apt-get purge nginx nginx-common nginx-full -y
sudo apt-get autoremove -y
sudo rm -rf /etc/nginx
sudo apt update
sudo apt install nginx -y
Шаг 2. Создание и настройка конфигурационного файла
Открыть файл конфигурации для сайта:
sudo nano /etc/nginx/sites-available/mysite.conf
Вставить следующий конфиг:
##
## NEWYEAR.TONICMAN.RU — FULL WORKING CONFIG
##
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name newyear.tonicman.ru;
root /var/www/html;
index index.html;
location /myip {
default_type text/plain;
return 200 "$remote_addr";
}
location / {
gzip off;
sub_filter_once on;
sub_filter
'И готов к демонстрации ✨'
'Ваш IP: $remote_addr';
try_files $uri $uri/ =404;
}
gzip on;
gzip_min_length 256;
gzip_types
text/css
application/javascript
application/json
application/xml
image/svg+xml;
ssl_certificate /etc/letsencrypt/live/newyear.tonicman.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/newyear.tonicman.ru/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
listen [::]:80;
server_name newyear.tonicman.ru;
return 301 https://$host$request_uri;
}
Либо конфигурация без IP и "И готов к демонстрации ":
##
## NEWYEAR.TONICMAN.RU — FULL WORKING CONFIG
##
# -------------------------
# HTTPS
# -------------------------
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name newyear.tonicman.ru;
root /var/www/html;
index index.html;
# -------------------------
# Route: /myip (optional, no JS conflict)
# -------------------------
location /myip {
default_type text/plain;
return 200 "$remote_addr";
}
# -------------------------
# Main site
# -------------------------
location / {
gzip off;
try_files $uri $uri/ =404;
}
# -------------------------
# gzip for assets only
# -------------------------
gzip on;
gzip_min_length 256;
gzip_types
text/css
application/javascript
application/json
application/xml
image/svg+xml;
# -------------------------
# SSL (Certbot)
# -------------------------
ssl_certificate /etc/letsencrypt/live/newyear.tonicman.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/newyear.tonicman.ru/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# -------------------------
# HTTP → HTTPS redirect
# -------------------------
server {
listen 80;
listen [::]:80;
server_name newyear.tonicman.ru;
return 301 https://$host$request_uri;
}
Шаг 3. Активация конфигурации
Создать символическую ссылку в sites-enabled:
sudo ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/
Шаг 4. Проверка конфигурации и перезапуск сервера
Проверить конфиг:
sudo nginx -t
Если команда выдала:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
то ошибок нет.
Перезагружаем Nginx:
sudo systemctl reload nginx
Шаг 5. (Опционально) Установка SSL-сертификата через Certbot
Установка certbot через snap
Удалить старый certbot, если он установлен:
sudo apt remove certbot
Установить certbot в snap:
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Получение сертификата:
sudo certbot --nginx
Шаблон index.html положить в
/var/www/html/
Код шаблона
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>🎄 Сервер онлайн</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
min-height: 220vh;
font-family: 'Segoe UI', Tahoma, sans-serif;
background: radial-gradient(circle at top, #0b2a55, #000814 70%);
color: #fff;
text-align: center;
overflow-x: hidden;
}
/* ===== ВЕРХНИЕ ГИРЛЯНДЫ ===== */
#garlandsTop {
position: fixed;
top: 0; left: 0;
width: 100%;
height: 200px;
pointer-events: none;
z-index: 50;
transition: none;
}
/* Вертикальная гирлянда для смартфонов */
@media (max-width: 480px) {
#garlandsTop {
width: 80px !important;
height: 100vh !important;
top: 0;
left: 0;
}
.hero {
padding-left: 100px;
}
.time {
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.time span {
min-width: 45%;
margin-bottom: 6px;
}
.time b {
font-size: 24px;
}
.time small {
font-size: 10px;
}
}
/* Заголовок */
h1 {
font-size: clamp(36px, 6vw, 64px);
margin: 0;
background: linear-gradient(90deg, #fff, #ffd86b, #fff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 40px rgba(255, 215, 120, 0.5);
}
.sub {
margin-top: 14px;
font-size: clamp(22px, 3vw, 32px);
background: linear-gradient(90deg, #cce3ff, #fff, #cce3ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 35px rgba(120, 180, 255, 0.6);
}
/* ===== ТАЙМЕР ===== */
.countdown {
margin-top: 28px;
font-size: 18px;
opacity: 0.95;
}
.time {
display: flex;
justify-content: center;
gap: 22px;
margin-top: 10px;
}
.time span {
display: flex;
flex-direction: column;
min-width: 72px;
}
.time b {
font-size: 34px;
background: linear-gradient(90deg, #fff, #ffd86b, #fff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.time small { font-size: 12px; opacity: 0.75; }
/* ===== ЁЛКА ===== */
.canvas-wrap {
margin: 130px auto 0;
width: 440px;
height: 560px;
}
#tree {
width: 100%;
height: 100%;
cursor: pointer;
}
/* ===== СНЕГ ===== */
.snow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
.snow span {
position: absolute;
top: -10px;
width: 6px; height: 6px;
background: #fff;
border-radius: 50%;
animation: fall linear infinite;
}
@keyframes fall {
to { transform: translateY(240vh); }
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
</style>
</head>
<body>
<canvas id="garlandsTop"></canvas>
<div class="hero">
<h1>Привет! Сервер работает.</h1>
<!-- ⚠️ НЕ МЕНЯТЬ — ДЛЯ IP -->
<div class="sub">И готов к демонстрации ✨</div>
<div class="countdown">
До Нового года осталось:
<div class="time">
<span><b id="d">0</b><small>дней</small></span>
<span><b id="h">0</b><small>часов</small></span>
<span><b id="m">0</b><small>мин</small></span>
<span><b id="s">0</b><small>сек</small></span>
</div>
</div>
<div class="canvas-wrap">
<canvas id="tree" width="440" height="560"></canvas>
</div>
</div>
<div class="snow" id="snow"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const gCanvas = document.getElementById('garlandsTop');
if (!gCanvas || !gCanvas.getContext) {
if (gCanvas) {
gCanvas.outerHTML = "<p style='color:#fff; text-align:center; padding:20px;'>Ваш браузер не поддерживает canvas. Обновите браузер для корректного отображения.</p>";
}
return;
}
const SNOW_COUNT = 170;
const FIREWORK_CHANCE = 0.05;
/* ===== СНЕГ ===== */
const snow = document.getElementById('snow');
for (let i = 0; i < SNOW_COUNT; i++) {
const s = document.createElement('span');
s.style.left = Math.random() * 100 + 'vw';
s.style.animationDuration = 10 + Math.random() * 15 + 's';
s.style.transform = `scale(${Math.random() + 0.3})`;
snow.appendChild(s);
}
/* ===== ФЕЙЕРВЕРК ===== */
class Firework {
constructor(x, y, ctx) {
this.x = x;
this.y = y;
this.ctx = ctx;
this.particles = [];
for (let i = 0; i < 100; i++) {
this.particles.push({
x: this.x,
y: this.y,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.5) * 5,
alpha: 1,
radius: 2 + Math.random() * 2,
});
}
}
update() {
this.particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.05;
p.alpha -= 0.015;
});
this.particles = this.particles.filter(p => p.alpha > 0);
}
draw() {
const ctx = this.ctx;
this.particles.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
ctx.shadowColor = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
ctx.shadowBlur = 10;
ctx.fill();
});
}
done() {
return this.particles.length === 0;
}
}
function showFireworks() {
const cw = window.innerWidth;
const ch = window.innerHeight;
const canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.left = 0;
canvas.style.top = 0;
canvas.width = cw;
canvas.height = ch;
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const fireworks = [];
function loop() {
ctx.clearRect(0, 0, cw, ch);
if (Math.random() < FIREWORK_CHANCE) {
fireworks.push(new Firework(Math.random() * cw, Math.random() * ch / 2, ctx));
}
fireworks.forEach(fw => {
fw.update();
fw.draw();
});
for (let i = fireworks.length - 1; i >= 0; i--) {
if (fireworks[i].done()) {
fireworks.splice(i, 1);
}
}
requestAnimationFrame(loop);
}
loop();
}
/* ===== ТАЙМЕР ===== */
const d = document.getElementById('d');
const h = document.getElementById('h');
const m = document.getElementById('m');
const s = document.getElementById('s');
function nextNewYear() {
const now = new Date();
let y = now.getFullYear();
if (now >= new Date(y, 0, 1)) y++;
return new Date(y, 0, 1, 0, 0, 0);
}
const eventTime = nextNewYear();
function updateTimer() {
const diff = eventTime - new Date();
if (diff <= 0) {
document.querySelector('.countdown').innerHTML = '<h2 style="font-size: 48px; color: gold; text-shadow: 0 0 10px #ff0, 0 0 20px #ff0;">С Новым годом!</h2>';
clearInterval(timerInterval);
showFireworks();
return;
}
d.textContent = Math.floor(diff / 86400000);
h.textContent = Math.floor(diff / 3600000) % 24;
m.textContent = Math.floor(diff / 60000) % 60;
s.textContent = (Math.floor(diff / 1000) % 60).toString().padStart(2, '0');
}
const timerInterval = setInterval(updateTimer, 1000);
updateTimer();
/* ===== ГИРЛЯНДА (CANVAS) ===== */
const gCtx = gCanvas.getContext('2d');
let isVertical = window.matchMedia("(max-width:480px)").matches;
function resizeGarlands() {
if (isVertical) {
gCanvas.width = 80;
gCanvas.height = window.innerHeight;
} else {
gCanvas.width = window.innerWidth;
gCanvas.height = 200;
}
}
resizeGarlands();
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
isVertical = window.matchMedia("(max-width:480px)").matches;
resizeGarlands();
}, 150);
});
const lightsCount = 80;
const topLights = new Array(lightsCount).fill(0).map((_, i) => ({
t: i / lightsCount,
hue: Math.random() * 360
}));
const verticalLights = new Array(lightsCount).fill(0).map((_, i) => ({
t: i / lightsCount,
hue: Math.random() * 360,
phase: Math.random() * Math.PI * 2
}));
function drawHorizontalGarland(t) {
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
const baseY = gCanvas.height / 2;
const amplitude = 25;
gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
gCtx.lineWidth = 3;
gCtx.beginPath();
for (let x = 0; x <= gCanvas.width; x += 20) {
const y = baseY + Math.sin(x * 0.01) * amplitude;
gCtx.lineTo(x, y);
}
gCtx.stroke();
topLights.forEach((l, i) => {
const x = l.t * gCanvas.width;
const y = baseY + Math.sin(x * 0.01) * amplitude;
const wave = (Math.sin(t * 0.003 + i * 0.4) + 1) / 2;
gCtx.beginPath();
gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.4 + wave * 0.6})`;
gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
gCtx.shadowBlur = 15 + wave * 25;
gCtx.arc(x, y, 7 + wave * 3, 0, Math.PI * 2);
gCtx.fill();
});
}
function drawVerticalGarland(t) {
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
const baseX = gCanvas.width / 2;
const amplitude = 15;
gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
gCtx.lineWidth = 3;
gCtx.beginPath();
for (let y = 0; y <= gCanvas.height; y += 20) {
const x = baseX + Math.sin(y * 0.02 + t * 0.002) * amplitude;
gCtx.lineTo(x, y);
}
gCtx.stroke();
verticalLights.forEach((l, i) => {
const y = l.t * gCanvas.height;
const x = baseX + Math.sin(y * 0.02 + t * 0.002 + l.phase) * amplitude;
const wave = (Math.sin(t * 0.005 + i * 0.6) + 1) / 2;
gCtx.beginPath();
gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.5 + wave * 0.5})`;
gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
gCtx.shadowBlur = 15 + wave * 20;
gCtx.arc(x, y, 8 + wave * 4, 0, Math.PI * 2);
gCtx.fill();
});
}
/* ===== ЁЛКА (CANVAS) ===== */
const tCanvas = document.getElementById('tree');
const tCtx = tCanvas.getContext('2d');
const branchPositions = [];
for (let y = 60; y < 440; y += 6) {
const w = ((y - 60) / 380) * 180;
for (let i = 0; i < 6; i++) {
branchPositions.push({
x: 220 + (Math.random() - 0.5) * w,
y: y,
});
}
}
const treeLights = new Array(45).fill(0).map(() => ({
x: 220 + (Math.random() - 0.5) * 160,
y: 100 + Math.random() * 300,
phase: Math.random() * Math.PI * 2
}));
let clickWave = 0;
tCanvas.addEventListener('click', () => clickWave = 1);
function drawTree(t) {
tCtx.clearRect(0, 0, 440, 560);
tCtx.fillStyle = 'hsl(145,45%,25%)';
branchPositions.forEach(({x, y}) => {
tCtx.fillRect(x, y, 2, 8);
});
treeLights.forEach(l => {
const glow = (Math.sin(t * 0.003 + l.phase) + 1) / 2 + clickWave;
tCtx.save();
tCtx.fillStyle = `rgba(255,220,150,${0.4 + glow * 0.5})`;
tCtx.shadowColor = 'rgba(255,220,150,1)';
tCtx.shadowBlur = 10 + glow * 20;
tCtx.beginPath();
tCtx.arc(l.x, l.y, 4 + glow * 2, 0, Math.PI * 2);
tCtx.fill();
tCtx.restore();
});
clickWave *= 0.92;
tCtx.shadowBlur = 0;
tCtx.fillStyle = '#6b3e1e';
tCtx.fillRect(205, 450, 30, 80);
tCtx.save();
tCtx.shadowBlur = 30;
tCtx.shadowColor = 'gold';
tCtx.fillStyle = 'gold';
tCtx.beginPath();
tCtx.arc(220, 40, 10, 0, Math.PI * 2);
tCtx.fill();
tCtx.restore();
}
function animate(t) {
if (isVertical) {
drawVerticalGarland(t);
} else {
drawHorizontalGarland(t);
}
drawTree(t);
requestAnimationFrame(animate);
}
animate(0);
});
</script>
</body>
</html>
Код шаблона без IP и текста И готов к демонстрации
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>🎄 Новый год</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
min-height: 220vh;
font-family: 'Segoe UI', Tahoma, sans-serif;
background: radial-gradient(circle at top, #0b2a55, #000814 70%);
color: #fff;
text-align: center;
overflow-x: hidden;
}
/* ===== ВЕРХНИЕ ГИРЛЯНДЫ ===== */
#garlandsTop {
position: fixed;
top: 0; left: 0;
width: 100%;
height: 200px;
pointer-events: none;
z-index: 50;
transition: none;
}
.hero {
padding-top: 140px;
}
@media (min-width: 1920px) {
.hero {
padding-top: 180px;
}
}
@media (max-width: 480px) {
#garlandsTop {
width: 80px !important;
height: 100vh !important;
top: 0;
left: 0;
}
.hero {
padding-left: 100px;
}
.time {
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.time span {
min-width: 45%;
margin-bottom: 6px;
}
.time b {
font-size: 24px;
}
.time small {
font-size: 10px;
}
}
/* ===== ТАЙМЕР ===== */
.countdown {
margin-top: 28px;
font-size: 18px;
opacity: 0.95;
}
.time {
display: flex;
justify-content: center;
gap: 22px;
margin-top: 10px;
}
.time span {
display: flex;
flex-direction: column;
min-width: 72px;
}
.time b {
font-size: 34px;
background: linear-gradient(90deg, #fff, #ffd86b, #fff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.time small { font-size: 12px; opacity: 0.75; }
/* ===== ЁЛКА ===== */
.canvas-wrap {
margin: 80px auto 0;
width: 440px;
height: 560px;
}
#tree {
width: 100%;
height: 100%;
cursor: pointer;
}
/* ===== СНЕГ ===== */
.snow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
.snow span {
position: absolute;
top: -10px;
width: 6px; height: 6px;
background: #fff;
border-radius: 50%;
animation: fall linear infinite;
}
@keyframes fall {
to { transform: translateY(240vh); }
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
</style>
</head>
<body>
<canvas id="garlandsTop"></canvas>
<div class="hero">
<div class="countdown">
До Нового года осталось:
<div class="time">
<span><b id="d">0</b><small>дней</small></span>
<span><b id="h">0</b><small>часов</small></span>
<span><b id="m">0</b><small>мин</small></span>
<span><b id="s">0</b><small>сек</small></span>
</div>
</div>
<div class="canvas-wrap">
<canvas id="tree" width="440" height="560"></canvas>
</div>
</div>
<div class="snow" id="snow"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const gCanvas = document.getElementById('garlandsTop');
if (!gCanvas || !gCanvas.getContext) {
if (gCanvas) {
gCanvas.outerHTML = "<p style='color:#fff; text-align:center; padding:20px;'>Ваш браузер не поддерживает canvas. Обновите браузер для корректного отображения.</p>";
}
return;
}
const SNOW_COUNT = 170;
const FIREWORK_CHANCE = 0.05;
/* ===== СНЕГ ===== */
const snow = document.getElementById('snow');
for (let i = 0; i < SNOW_COUNT; i++) {
const s = document.createElement('span');
s.style.left = Math.random() * 100 + 'vw';
s.style.animationDuration = 10 + Math.random() * 15 + 's';
s.style.transform = `scale(${Math.random() + 0.3})`;
snow.appendChild(s);
}
/* ===== ФЕЙЕРВЕРК ===== */
class Firework {
constructor(x, y, ctx) {
this.x = x;
this.y = y;
this.ctx = ctx;
this.particles = [];
for (let i = 0; i < 100; i++) {
this.particles.push({
x: this.x,
y: this.y,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.5) * 5,
alpha: 1,
radius: 2 + Math.random() * 2,
});
}
}
update() {
this.particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.05;
p.alpha -= 0.015;
});
this.particles = this.particles.filter(p => p.alpha > 0);
}
draw() {
const ctx = this.ctx;
this.particles.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
ctx.shadowColor = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
ctx.shadowBlur = 10;
ctx.fill();
});
}
done() {
return this.particles.length === 0;
}
}
function showFireworks() {
const cw = window.innerWidth;
const ch = window.innerHeight;
const canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.left = 0;
canvas.style.top = 0;
canvas.width = cw;
canvas.height = ch;
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const fireworks = [];
function loop() {
ctx.clearRect(0, 0, cw, ch);
if (Math.random() < FIREWORK_CHANCE) {
fireworks.push(new Firework(Math.random() * cw, Math.random() * ch / 2, ctx));
}
fireworks.forEach(fw => {
fw.update();
fw.draw();
});
for (let i = fireworks.length - 1; i >= 0; i--) {
if (fireworks[i].done()) {
fireworks.splice(i, 1);
}
}
requestAnimationFrame(loop);
}
loop();
}
/* ===== ТАЙМЕР ===== */
const d = document.getElementById('d');
const h = document.getElementById('h');
const m = document.getElementById('m');
const s = document.getElementById('s');
function nextNewYear() {
const now = new Date();
let y = now.getFullYear();
if (now >= new Date(y, 0, 1)) y++;
return new Date(y, 0, 1, 0, 0, 0);
}
const eventTime = nextNewYear();
function updateTimer() {
const diff = eventTime - new Date();
if (diff <= 0) {
document.querySelector('.countdown').innerHTML = '<h2 style="font-size: 48px; color: gold; text-shadow: 0 0 10px #ff0, 0 0 20px #ff0;">С Новым годом!</h2>';
clearInterval(timerInterval);
showFireworks();
return;
}
d.textContent = Math.floor(diff / 86400000);
h.textContent = Math.floor(diff / 3600000) % 24;
m.textContent = Math.floor(diff / 60000) % 60;
s.textContent = (Math.floor(diff / 1000) % 60).toString().padStart(2, '0');
}
const timerInterval = setInterval(updateTimer, 1000);
updateTimer();
/* ===== ГИРЛЯНДА (CANVAS) ===== */
const gCtx = gCanvas.getContext('2d');
let isVertical = window.matchMedia("(max-width:480px)").matches;
function resizeGarlands() {
if (isVertical) {
gCanvas.width = 80;
gCanvas.height = window.innerHeight;
} else {
gCanvas.width = window.innerWidth;
gCanvas.height = 200;
}
}
resizeGarlands();
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
isVertical = window.matchMedia("(max-width:480px)").matches;
resizeGarlands();
}, 150);
});
const lightsCount = 80;
const topLights = new Array(lightsCount).fill(0).map((_, i) => ({
t: i / lightsCount,
hue: Math.random() * 360
}));
const verticalLights = new Array(lightsCount).fill(0).map((_, i) => ({
t: i / lightsCount,
hue: Math.random() * 360,
phase: Math.random() * Math.PI * 2
}));
function drawHorizontalGarland(t) {
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
const baseY = gCanvas.height / 2;
const amplitude = 25;
gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
gCtx.lineWidth = 3;
gCtx.beginPath();
for (let x = 0; x <= gCanvas.width; x += 20) {
const y = baseY + Math.sin(x * 0.01) * amplitude;
gCtx.lineTo(x, y);
}
gCtx.stroke();
topLights.forEach((l, i) => {
const x = l.t * gCanvas.width;
const y = baseY + Math.sin(x * 0.01) * amplitude;
const wave = (Math.sin(t * 0.003 + i * 0.4) + 1) / 2;
gCtx.beginPath();
gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.4 + wave * 0.6})`;
gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
gCtx.shadowBlur = 15 + wave * 25;
gCtx.arc(x, y, 7 + wave * 3, 0, Math.PI * 2);
gCtx.fill();
});
}
function drawVerticalGarland(t) {
gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
const baseX = gCanvas.width / 2;
const amplitude = 15;
gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
gCtx.lineWidth = 3;
gCtx.beginPath();
for (let y = 0; y <= gCanvas.height; y += 20) {
const x = baseX + Math.sin(y * 0.02 + t * 0.002) * amplitude;
gCtx.lineTo(x, y);
}
gCtx.stroke();
verticalLights.forEach((l, i) => {
const y = l.t * gCanvas.height;
const x = baseX + Math.sin(y * 0.02 + t * 0.002 + l.phase) * amplitude;
const wave = (Math.sin(t * 0.005 + i * 0.6) + 1) / 2;
gCtx.beginPath();
gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.5 + wave * 0.5})`;
gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
gCtx.shadowBlur = 15 + wave * 20;
gCtx.arc(x, y, 8 + wave * 4, 0, Math.PI * 2);
gCtx.fill();
});
}
/* ===== ЁЛКА (CANVAS) ===== */
const tCanvas = document.getElementById('tree');
const tCtx = tCanvas.getContext('2d');
const branchPositions = [];
for (let y = 60; y < 440; y += 6) {
const w = ((y - 60) / 380) * 180;
for (let i = 0; i < 6; i++) {
branchPositions.push({
x: 220 + (Math.random() - 0.5) * w,
y: y,
});
}
}
const treeLights = new Array(45).fill(0).map(() => ({
x: 220 + (Math.random() - 0.5) * 160,
y: 100 + Math.random() * 300,
phase: Math.random() * Math.PI * 2
}));
let clickWave = 0;
tCanvas.addEventListener('click', () => clickWave = 1);
function drawTree(t) {
tCtx.clearRect(0, 0, 440, 560);
tCtx.fillStyle = 'hsl(145,45%,25%)';
branchPositions.forEach(({x, y}) => {
tCtx.fillRect(x, y, 2, 8);
});
treeLights.forEach(l => {
const glow = (Math.sin(t * 0.003 + l.phase) + 1) / 2 + clickWave;
tCtx.save();
tCtx.fillStyle = `rgba(255,220,150,${0.4 + glow * 0.5})`;
tCtx.shadowColor = 'rgba(255,220,150,1)';
tCtx.shadowBlur = 10 + glow * 20;
tCtx.beginPath();
tCtx.arc(l.x, l.y, 4 + glow * 2, 0, Math.PI * 2);
tCtx.fill();
tCtx.restore();
});
clickWave *= 0.92;
tCtx.shadowBlur = 0;
tCtx.fillStyle = '#6b3e1e';
tCtx.fillRect(205, 450, 30, 80);
tCtx.save();
tCtx.shadowBlur = 30;
tCtx.shadowColor = 'gold';
tCtx.fillStyle = 'gold';
tCtx.beginPath();
tCtx.arc(220, 40, 10, 0, Math.PI * 2);
tCtx.fill();
tCtx.restore();
}
function animate(t) {
if (isVertical) {
drawVerticalGarland(t);
} else {
drawHorizontalGarland(t);
}
drawTree(t);
requestAnimationFrame(animate);
}
animate(0);
});
</script>
</body>
</html>